﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using Dataweb.NShape;
using Dataweb.NShape.Advanced;
using Dataweb.NShape.Commands;
using Dataweb.NShape.GeneralShapes;
using Dataweb.NShape.Layouters;
using Dataweb.NShape.SoftwareArchitectureShapes;
using Dataweb.NShape.WinFormsUI;
using DBCodeBuilder.Core.Classes;
using DBCodeBuilder.Core.Enum;
using DBCodeBuilder.Core.Models;
using HSystem.Data.DbMetadataClasses;
using Label = Dataweb.NShape.GeneralShapes.Label;

namespace DBCodeBuilder.Core
{
    //GOD BLESS THE TAG
    internal class GraphicHelper
    {
        private const int _defaultHeightForEachColumn = 30;
        private const int _defaultTableHeightOffset = 50;
        private const int _defaultTableWidthOffset = 15;

        private CachedRepository Repository;
        private Diagram TableDiagram;
        private Display TableDisplay;
        private Project Project;
        private ShapeType TableShape, RelationshipShape, LblShape;
        private Font baseEntityFont;

        public bool TableDiagramInRepositroy
        {
            get { return Project.Repository.GetDiagrams().Contains(TableDiagram); }
        }

        #region Initialization

        public GraphicHelper(CachedRepository repository, Project project, Diagram tableDiagram, Display tableDisplay)
        {
            Project = project;
            Repository = repository;
            TableDiagram = tableDiagram;
            TableDisplay = tableDisplay;
        }

        public void SetDefaultShapes(ShapeType tableShape, ShapeType relationshipShape, ShapeType lblShape)
        {
            TableShape = tableShape;
            RelationshipShape = relationshipShape;
            LblShape = lblShape;

            var charStyle = ((EntitySymbol)tableShape.CreateInstance()).ColumnCharacterStyle;
            baseEntityFont = new Font(charStyle.FontFamily, charStyle.SizeInPoints, FontStyle.Bold);
        }

        #endregion

        #region Draw

        public void DrawTables(List<TableModel> tables, int? X = null, int? Y = null)
        {
            foreach (TableModel table in tables)
                DrawTable(table, X, Y);

            DrawTableRelationships(tables);
        }

        private void DrawTableRelationships(List<TableModel> tables, bool addToRepository = false)
        {
            IEnumerable<Tuple<string, string>> linkedTables = GetLinkedTables(tables);
            foreach (var kvpLink in linkedTables)
                DrawTableLink(kvpLink, tables, addToRepository);
        }

        public void DrawTable(TableModel table, int? X = null, int? Y = null, bool addToRepository = false)
        {
            int maxWidth;
            EntitySymbol et = (EntitySymbol)TableShape.CreateInstance();

            et.FillStyle = table.ObjectType == DbObjectType.Table
              ? (FillStyle)Project.Design.FindStyleByName("fillStyleTable", typeof(FillStyle))
              : (FillStyle)Project.Design.FindStyleByName("fillStyleView", typeof(FillStyle));

            AddcolumnsToEntitySymbol(et, table.MetaData, out maxWidth);
            et.Text = String.IsNullOrEmpty(table.UserMapping.ClassName) ? table.MetaData.Name : table.UserMapping.ClassName;

            //In 'Shape' form, only the Tag can be ecffectively used as identifier
            et.Tag = table.MetaData.Name;

            if (table.ShapePlacementInfo == null && X != null && Y != null)
            {
                et.X = X.Value;
                et.Y = Y.Value;

                et.Width = maxWidth + _defaultTableWidthOffset;
                et.Height = table.MetaData.Columns.Count * _defaultHeightForEachColumn + _defaultTableHeightOffset;
            }
            else if (table.ShapePlacementInfo == null && X == null && Y == null)
            {
                et.X = TableDiagram.Width / 2;
                et.Y = TableDiagram.Height / 2;
                et.Width = maxWidth + _defaultTableWidthOffset;
                et.Height = table.MetaData.Columns.Count * _defaultHeightForEachColumn + _defaultTableHeightOffset;
            }
            else if (table.ShapePlacementInfo != null)
            {
                et.X = table.ShapePlacementInfo.Location.X;
                et.Y = table.ShapePlacementInfo.Location.Y;

                et.Width = table.ShapePlacementInfo.Size.Width;
                et.Height = table.ShapePlacementInfo.Size.Height;
            }

            //After placing the object, registers the placement informations
            if (table.ShapePlacementInfo == null)
                table.ShapePlacementInfo = new ShapePlacementInformation
                {
                    Location = new Point(et.X, et.Y),
                    Size = new Size(et.Width, et.Height),
                    ShapeTag = (string)et.Tag
                };

            TableDiagram.Shapes.Add(et, Project.Repository.ObtainNewTopZOrder(TableDiagram));
            if (addToRepository)
                Repository.Insert((Shape)et, TableDiagram);
        }

        private void DrawTableLink(Tuple<string, string> kvpLink, List<TableModel> tablesContainer, bool addToRepository)
        {
            var fk = DrawRelationshipLine(kvpLink, tablesContainer, addToRepository);
            DrawRelationshipLabel(kvpLink, fk, tablesContainer, addToRepository);
        }

        private void DrawRelationshipLabel(Tuple<string, string> kvpLink, RectangularLine fk, List<TableModel> tablesContainer, bool addToRepository)
        {
            //Get a point in the middle of the line and create there a control point
            Point fkMiddlePoint = GetLineApproximateMiddleConnectionPoint(fk);
            var retPoint = fk.AddConnectionPoint(fkMiddlePoint.X, fkMiddlePoint.Y);

            //If i have a Parent-Children kind of association (say, a tree representation in the DB model), 
            //the graphic would go boom so i just ignore it
            if (kvpLink.Item1 == kvpLink.Item2)
                return;

            var rel = GetRelationshipByTables(tablesContainer, kvpLink.Item1, kvpLink.Item2);
            var direction = GetLabelDirectionByRelationshipEntities(fk, tablesContainer);

            Label lbl = (Label)LblShape.CreateInstance();
            if (!rel.Generate)
            {
                lbl.CharacterStyle = (ICharacterStyle)Project.Design.FindStyleByName("LabelStyleNotGen", typeof(CharacterStyle));
                fk.LineStyle = (ILineStyle)Project.Design.FindStyleByName("RelNotGenerated", typeof(LineStyle));
            }
            else
            {
                lbl.CharacterStyle = (ICharacterStyle)Project.Design.FindStyleByName("LabelStyle", typeof(CharacterStyle));
                fk.LineStyle = (ILineStyle)Project.Design.FindStyleByName("RelGenerated", typeof(LineStyle));
            }

            if (rel.RelType == DbRelationshipType.OneToOne)
                lbl.Text = "<-1...1->";
            else
            {
                if (direction == LabelDirection.Right)
                    lbl.Text = "--N...1->";
                else if (direction == LabelDirection.Left)
                    lbl.Text = "<-1...N-";
            }
            lbl.Tag = String.Format("{0}|{1}L", kvpLink.Item1, kvpLink.Item2);
            lbl.CharacterStyle = Project.Design.CharacterStyles["LabelStyle"];

            lbl.Connect(lbl.GetControlPointIds(ControlPointCapabilities.Glue).ToList()[0], fk, retPoint);
            TableDiagram.Shapes.Add(lbl, Project.Repository.ObtainNewBottomZOrder(TableDiagram));

            if (addToRepository)
                Repository.Insert((Shape)lbl, TableDiagram);
        }

        private RectangularLine DrawRelationshipLine(Tuple<string, string> kvpLink,
            IEnumerable<TableModel> tablesContainer, bool addToRepository)
        {
            RectangularLine fk = (RectangularLine)RelationshipShape.CreateInstance();
            fk.LineStyle = Project.Design.LineStyles.Thick;
            fk.EndCapStyle = Project.Design.CapStyles.ClosedArrow;

            var relType = GetRelationshipByTables(tablesContainer, kvpLink.Item1, kvpLink.Item2).RelType;

            if (relType == DbRelationshipType.OneToOne)
                fk.StartCapStyle = Project.Design.CapStyles.ClosedArrow;

            fk.Connect(ControlPointId.FirstVertex, TableDiagram.Shapes.Single(t => (string)t.Tag == kvpLink.Item1),
                ControlPointId.Reference);
            fk.Connect(ControlPointId.LastVertex, TableDiagram.Shapes.Single(t => (string)t.Tag == kvpLink.Item2),
                ControlPointId.Reference);
            fk.Tag = String.Format("{0}|{1}R", kvpLink.Item1, kvpLink.Item2);
            TableDiagram.Shapes.Add(fk, Project.Repository.ObtainNewBottomZOrder(TableDiagram));

            if (addToRepository)
                Repository.Insert((Shape)fk, TableDiagram);
            return fk;
        }

        #endregion

        #region Update

        public void UpdateTableRelationships(List<TableModel> tables)
        {
            CleanupTableRelationships();
            DrawTableRelationships(tables, true);
        }

        public void UpdateTableShape(TableModel newModel)
        {
            var toUpd = TableDiagram.Shapes.Single(t => (string)t.Tag == newModel.Name);

            ((EntitySymbol)toUpd).Text = String.IsNullOrEmpty(newModel.UserMapping.ClassName) ? ((EntitySymbol)toUpd).Text : newModel.UserMapping.ClassName;
            Repository.Update(toUpd);
        }

        public void UpdateRelationshipLine(DbRelationship newRel)
        {
            var toUpdRel = TableDiagram.Shapes.Single(t => (string)t.Tag == String.Format("{0}|{1}R", newRel.OriginalTable, newRel.RefTable));

            switch (newRel.RelType)
            {
                case DbRelationshipType.OneToOne:
                    ((RectangularLine)toUpdRel).StartCapStyle = Project.Design.CapStyles.ClosedArrow;
                    ((RectangularLine)toUpdRel).EndCapStyle = Project.Design.CapStyles.ClosedArrow;
                    break;
                case DbRelationshipType.ManyToOne:
                    ((RectangularLine)toUpdRel).StartCapStyle = Project.Design.CapStyles.None;
                    ((RectangularLine)toUpdRel).EndCapStyle = Project.Design.CapStyles.ClosedArrow;
                    break;
            }
            Repository.Update(toUpdRel);
        }

        public void UpdateRelationshipLabel(DbRelationship newRel, List<TableModel> tableContainer = null)
        {
            int count = TableDiagram.Shapes.Count(t => (string)t.Tag == String.Format("{0}|{1}L", newRel.OriginalTable, newRel.RefTable));
            if (count > 1)
                return;

            var toUpdLabel = (Label)TableDiagram.Shapes.SingleOrDefault(t => (string)t.Tag == String.Format("{0}|{1}L", newRel.OriginalTable, newRel.RefTable));
            var rel = (RectangularLine)TableDiagram.Shapes.SingleOrDefault(t => (string)t.Tag == String.Format("{0}|{1}R", newRel.OriginalTable, newRel.RefTable));

            if (rel == null && toUpdLabel == null)
                return;

            if (!newRel.Generate)
            {
                toUpdLabel.CharacterStyle = (ICharacterStyle)Project.Design.FindStyleByName("LabelStyleNotGen", typeof(CharacterStyle));
                rel.LineStyle = (ILineStyle)Project.Design.FindStyleByName("RelNotGenerated", typeof(LineStyle));
            }
            else
            {
                toUpdLabel.CharacterStyle = (ICharacterStyle)Project.Design.FindStyleByName("LabelStyle", typeof(CharacterStyle));
                rel.LineStyle = (ILineStyle)Project.Design.FindStyleByName("RelGenerated", typeof(LineStyle));
            }

            switch (newRel.RelType)
            {
                case DbRelationshipType.OneToOne:
                    toUpdLabel.Text = "<-1...1->";
                    break;
                case DbRelationshipType.ManyToOne:
                    if (tableContainer == null)
                    {
                        toUpdLabel.Text = "--N...1->";
                        break;
                    }

                    var direction = GetLabelDirectionByRelationshipEntities(rel, tableContainer);
                    if (direction == LabelDirection.Left)
                        toUpdLabel.Text = "<-1...N--";
                    else if (direction == LabelDirection.Right)
                        toUpdLabel.Text = "--N...1->";
                    break;
            }
            Repository.Update((Shape)toUpdLabel);
            Repository.Update((Shape)rel);
        }

        internal void UpdateRelationshipLabels(List<DbRelationship> rels, List<TableModel> tableContainer)
        {
            foreach (var dbRel in rels)
                UpdateRelationshipLabel(dbRel, tableContainer);
        }

        #endregion

        #region Cleanup

        private void CleanupTableRelationships()
        {
            var rels = GetAllRelationships();
            var labels = GetAllLabels();

            if (rels.Any())
                DeleteShapes(rels);

            if (labels.Any())
                DeleteShapes(labels);
        }

        public void CleanTableDrawspace()
        {
            if (TableDiagram == null)
                return;

            TableDiagram.Clear();
            TableDisplay.Clear();

            if (Project.Repository.GetDiagrams().Contains(TableDiagram))
                Project.Repository.DeleteAll(TableDiagram);
        }

        public void RemoveLinkedShapes(List<EntitySymbol> removed)
        {
            foreach (EntitySymbol et in removed)
            {
                var relationships = GetRelationshipsByEntity(et);
                var labels = GetRelationshipLabelsByEntity(et);

                if (relationships.Any())
                    DeleteShapes(relationships);
                if (labels.Any())
                    DeleteShapes(labels);
            }
        }

        private void DeleteShapes(IEnumerable<Shape> labels)
        {
            DeleteShapesCommand cmdLbl = new DeleteShapesCommand(Repository, TableDiagram, labels, false);
            cmdLbl.Execute();
        }

        #endregion

        #region Presentation

        private void AdjustZoomToDiagram(Display dsp, Diagram dispDiagram)
        {
            int minX = int.MaxValue, maxX = int.MinValue, minY = int.MaxValue, maxY = int.MinValue;
            var shapes = dispDiagram.Shapes.ToList();
            foreach (Shape shape in shapes)
            {
                var shapeRect = shape.GetBoundingRectangle(true);
                if (shapeRect.X < minX)
                    minX = shapeRect.X;

                if (shapeRect.Y < minY)
                    minY = shapeRect.Y;

                if (shapeRect.X + shapeRect.Width > maxX)
                    maxX = shapeRect.X + shapeRect.Width;

                if (shapeRect.Y + shapeRect.Height > maxY)
                    maxY = shapeRect.Y + shapeRect.Height;
            }

            var viewRect = new Rectangle(minX, minY, maxX - minX, maxY - minY);

            dsp.ZoomLevel = 400;
            dsp.EnsureVisible(dispDiagram.Shapes.ElementAt(0));
            dsp.EnsureVisible(viewRect);
            dsp.ZoomLevel -= (dsp.ZoomLevel / 5) * 2;
        }

        public void PerformTableLayouting(List<TableModel> tableContainer)
        {
            RepulsionLayouter layouter = new RepulsionLayouter(Project)
            {
                SpringRate = 10,
                Repulsion = 40,
                RepulsionRange = 510,
                Friction = 0,
                Mass = 1200,
                AllShapes = TableDiagram.Shapes,
                Shapes = TableDiagram.Shapes
            };

            layouter.Prepare();
            layouter.Execute(10);
            layouter.Fit(50, 50, TableDiagram.Width - 100, TableDiagram.Height - 100);

            foreach (var tabModel in tableContainer)
            {
                var shape = TableDiagram.Shapes.Single(t => t.Tag.ToString() == tabModel.Name);
                tabModel.ShapePlacementInfo.Location = new Point(shape.X, shape.Y);
            }
        }

        public void AssignTableDiagram(bool adjustZoom)
        {
            TableDisplay.Diagram = TableDiagram;
            if (adjustZoom)
                AdjustZoomToDiagram(TableDisplay, TableDiagram);
        }

        public void PopupLayouter(Form owner)
        {
            LayoutDialog layout = new LayoutDialog
            {
                Project = Project,
                Diagram = TableDiagram,
                SelectedShapes = TableDiagram.Shapes.Where(t => t.GetType() != typeof(Label))
            };
            layout.Show(owner);
        }

        #endregion

        #region Utility

        public void AssignTableRepositoryIfNecessary()
        {
            if (!Project.Repository.GetDiagrams().Contains(TableDiagram))
                Repository.InsertAll(TableDiagram);
        }

        public DbRelationship GetDBRelByRelationshipLine(RectangularLine line, List<TableModel> tablesContainer)
        {
            var tag = line.Tag.ToString();
            var split = tag.Remove(tag.Length - 1).Split('|');
            var rel = GetRelationshipByTables(tablesContainer, split[0], split[1]);
            return rel;
        }

        private Point GetLineApproximateMiddleConnectionPoint(RectangularLine fk)
        {
            var rect = fk.GetBoundingRectangle(true);
            int startX = rect.X;
            int startY = rect.Y;
            int endX = startX + rect.Width;
            int endY = startY + rect.Height;
            Point intersectPointY = new Point();

            for (int y = startY; y <= endY; y++)
            {
                Point p = new Point((startX + endX) / 2, y);
                if (!fk.ContainsPoint(p.X, p.Y)) continue;

                intersectPointY = p;
                break;
            }

            return intersectPointY;
        }

        private DbRelationship GetRelationshipByTables(IEnumerable<TableModel> tables, string tab1, string tab2)
        {
            var tab = tables.Single(t => t.Name == tab1);
            DbRelationship rel = tab.References.First(t => t.RefTable == tab2);
            return rel;
        }

        private IEnumerable<Tuple<string, string>> GetLinkedTables(List<TableModel> tables)
        {
            var ret = new List<Tuple<string, string>>();
            foreach (var table in tables)
            {
                var fks = table.MetaData.ForeignKeys.Where(t => t.IsParent).ToList();

                if (!fks.Any())
                    continue;

                foreach (var fk in fks.Where(fk => tables.SingleOrDefault(t => t.Name == fk.Reference.TableName) != null))
                {
                    if (table.Name != fk.Reference.TableName)
                        ret.Add(new Tuple<string, string>(table.Name, fk.Reference.TableName));

                    if (!table.References.Any(t => t.RefOwner == fk.Reference.Owner && t.RefTable == fk.Reference.TableName && t.NameOfRepresentingFK == fk.Name))
                    {
                        table.References.Add(new DbRelationship
                        {
                            RefOwner = fk.Reference.Owner,
                            RefTable = fk.Reference.TableName,
                            RelType = DbRelationshipType.ManyToOne,
                            OriginalTable = table.Name,
                            Generate = true,
                            GenerateInverse = false,
                            CascadeStyleDirect = DbCascadeStyle.None,
                            CascadeStyleInverse = DbCascadeStyle.None,
                            LazyLoadingDirect = true,
                            LazyLoadingInverse = false,
                            NullableDirect = false,
                            NameOfRepresentingFK = fk.Name
                        });
                    }
                }
            }

            return ret;
        }

        public Shape GetRelationshipByLabel(Label label)
        {
            var tag = label.Tag.ToString();
            var count = TableDiagram.Shapes.Count(t => t.Tag.ToString() == tag.Remove(tag.Length - 1) + "R");
            if (count > 1)
                return null;
            //Sostituire solo l'ultimo carattere
            return TableDiagram.Shapes.Single(t => t.Tag.ToString() == tag.Remove(tag.Length - 1) + "R");
        }

        internal List<Shape> GetLinkedShapes(EntitySymbol selectedEntity)
        {
            List<Shape> matchingShapes = new List<Shape>();

            var rels = GetRelationshipsByEntity(selectedEntity);
            matchingShapes.AddRange(rels);
            matchingShapes.AddRange(GetRelationshipLabelsByEntity(selectedEntity));
            foreach (var rel in rels)
                matchingShapes.AddRange(GetEntitiesByRelationship(rel).Excluding(selectedEntity));

            return matchingShapes;
        }

        private LabelDirection GetLabelDirectionByRelationshipEntities(RectangularLine fk, IEnumerable<TableModel> tablesContainer)
        {
            List<EntitySymbol> ents = GetEntitiesByRelationship(fk);
            var TablesEntities = tablesContainer.Where(t => ents.Select(ta => ta.Tag).Contains(t.Name)).ToList();

            TableModel ManyEntity = TablesEntities.FirstOrDefault(k => k.MetaData.ForeignKeys.Any(l => l.IsParent && TablesEntities.Select(i => i.Name).Contains(l.Reference.TableName)));
            TableModel OneEntity = TablesEntities.FirstOrDefault(k => k.MetaData.ForeignKeys.Any(l => !l.IsParent && TablesEntities.Select(i => i.Name).Contains(l.Reference.TableName)));

            //Workaround for inverse foreign keys that couldn't be retrieved
            if (OneEntity == null)
                OneEntity = TablesEntities.Single(t => t.Name != ManyEntity.Name);

            var OneEntityX = OneEntity.ShapePlacementInfo.Location.X;
            var ManyEntityX = ManyEntity.ShapePlacementInfo.Location.X;

            if (OneEntityX < ManyEntityX)
                return LabelDirection.Left;

            if (ManyEntityX < OneEntityX)
                return LabelDirection.Right;

            return LabelDirection.Left;
        }

        private List<EntitySymbol> GetEntitiesByRelationship(RectangularLine selectedEntity)
        {
            var shapes = GetAllEntities();
            var tagLength = selectedEntity.Tag.ToString().Length;
            return shapes.Where(t => selectedEntity.Tag.ToString().Substring(0, tagLength - 1).Split('|').Contains(t.Tag.ToString())).ToList();
        }

        public List<RectangularLine> GetRelationshipsByEntity(EntitySymbol entity)
        {
            var shapes = GetAllRelationships();
            return shapes.Where(shape => shape.Tag.ToString().Substring(0, shape.Tag.ToString().Length - 1).Split('|').Contains((string)entity.Tag)).ToList();
        }

        private List<RectangularLine> GetAllRelationships()
        {
            return TableDiagram.Shapes.Where(t => t.GetType() == typeof(RectangularLine)).Cast<RectangularLine>().ToList();
        }

        public List<EntitySymbol> GetAllEntities()
        {
            return TableDiagram.Shapes.Where(t => t.GetType() == typeof(EntitySymbol)).Cast<EntitySymbol>().ToList();
        }

        public List<Label> GetRelationshipLabelsByEntity(EntitySymbol entity)
        {
            var shapes = GetAllLabels();
            return shapes.Where(shape => shape.Tag.ToString().Contains((string)entity.Tag)).ToList();
        }

        private List<Label> GetAllLabels()
        {
            return TableDiagram.Shapes.Where(t => t.GetType() == typeof(Label)).Cast<Label>().ToList();
        }

        private void AddcolumnsToEntitySymbol(EntitySymbol et, MetaDataTable metadati, out int maxWidth)
        {
            maxWidth = TextRenderer.MeasureText(metadati.Name, new Font(baseEntityFont.FontFamily, baseEntityFont.Size + 3)).Width;
            var names = metadati.Columns.Select(t => t.Name);

            foreach (string name in names)
            {
                et.AddColumn(name);
                maxWidth = TextRenderer.MeasureText(name, baseEntityFont).Width > maxWidth ? TextRenderer.MeasureText(name, baseEntityFont).Width : maxWidth;
            }
        }

        internal static List<EntitySymbol> GetOnlyEntities(List<Shape> shapes)
        {
            return shapes.Where(shape => shape.GetType() == typeof(EntitySymbol)).Cast<EntitySymbol>().ToList();
        }

        internal static List<RectangularLine> GetOnlyRelationships(List<Shape> shapes)
        {
            return shapes.Where(shape => shape.GetType() == typeof(RectangularLine)).Cast<RectangularLine>().ToList();
        }

        internal static List<Label> GetOnlyLabels(List<Shape> shapes)
        {
            return shapes.Where(shape => shape.GetType() == typeof(Label)).Cast<Label>().ToList();
        }

        #endregion
    }
}